我们继续制作弹幕shooter,上一章我们构思了一下游戏的技术需求以及做了一些基本的通用性的框架,这一章我们来思考一下游戏对象的设计问题。
弹幕shooter(II)
游戏对象
我们在基础教程里曾经说过游戏对象的问题。因为我们决定使用oop的方式组织代码,所以游戏对象是既有数据又有功能函数的集合。而由于数据和功能一般有继承关系,因此结构是树状的。(相比而言,组件式编程的游戏对象仅仅含有数据,所有游戏功能是接入的形式的,因此结构是集合形式的。)
树状的类结构,我们要实现功能的不断继承和叠加。也就是从最基本的功能开始,到最个别的功能。但是,oop是辅助你写程序的形式,而非你的绊脚石,因此,不必为了写复杂层级而写,而是按自己需求,放心,额外的代码重复不会造成程序的拖延。
游戏对象设计
对于一般的游戏引擎而言,比如cocos他的基本对象是节点,带图片的就是精灵了。精灵作为最基本的游戏对象,它有下面的一些特征。
- 位置
位置是控制对象在游戏坐标系的位置的参数,一般有x,y,游戏游戏还需要z序来控制层叠的顺序。对于位置的延伸,可能还包括速度、加速度、帧位移等内容。当然还有旋转以及旋转锚点。
- 循环控制
还拿cocos举例,只要你把某个节点加入到场景中,就可以自动循环了,而love因为全是手动控制,因此需要你自己添加。比如我们游戏沙盒有个game.objects={}容器,我们每次生成的时候,把他们添加进去就行了。而game的循环控制会帮你循环。
- 更新
我们刚刚提到了循环,而循环的意义实际上就是逻辑更新和绘图更新。而对于精灵而言,更新的意义可能是位置的更新,以及帧动画的更新。这里提一些额外的话,love的编程模式是回调式的,也就是在控制流中不停的跳转到项目的各个角落。但是,回调的嵌套往往会造成一些麻烦,比如已经嵌套的回调,你不太容易重新指定回调的顺序,或者移除某些嵌套的回调。还有一种控制形式是基于消息和事件的,而精灵会在更新逻辑中监听系统的事件和消息,如果有自己相关的事件广播或者有发向自己的消息,那么就处理相应的逻辑,这种方式的好处是,某个精灵不会保存外部的ref,使得代码很干净,通过对消息系统的监听也比较容易的debug,消息系统配合调度器系统也比较好用(相比延迟回调而言)。不过消息系统的效率往往不及回调,而且思路上也比较复杂,因此按个人需求自己选择用那种模式。
- 绘制
绘制分为两部分,一部分是绘制的数据,一部分是绘制的回调。
从数据角度,我们可能需要image,quad,mesh,shader,canvas,animation等一大堆对象,以及颜色,混色方式,遮罩等控制,不过根据我们目前的项目而言,可以简化。
从绘制回调,我们一般要把绘制对象按其位置、角度、缩放一一绘制。同时还要注意绘制顺序问题。
- 销毁
销毁过程主要是释放一些无法自动释放的东西,比如保存在外部容器的对象。同时,在游戏沙盒中注销自己。
另外还要注意类的私有变量、公开变量及实例的变量。可能在lua中的oop并不是显式指明那些是私有,哪些是公开,哪些是受保护的,而且任何时候,任何位置的代码只要能够上查到他们,都可以编辑这些变量,使得Lua中的oop往往会存在一些陷阱。所以一定要理解这几种变量的作用、区别与定义方法。
游戏对象结构
base 游戏对象的基类,提供上文提到的循环加入与移除,基本绘图,基本的逻辑更新以及位置更新。一般而言,base可以用来表示一些没有互动的物体,比如背景图片,基本的UI内容等。
collidable 从base继承,加入碰撞初始化,碰撞筛选和碰撞遍历。绘制加入aabb的debugdraw方法。用来表示一些有碰撞却没有互动的物体,比如一些有碰撞的残骸等等。
bullet 从collidable继承,主要就是碰撞的检测了。以及特殊子弹的移动行为及特殊的绘制。
ship 从collidable继承,加入一些飞机的属性和行为,比如飞机的引擎火焰(从base继承),飞机的武器系统,飞机的移动控制,飞机摧毁(用来移除飞机火焰对象),飞机特有碰撞,飞机被弹,飞机开火等等。它是敌机和我机的基类,同时也可以是其他飞机的基类,比如飞机爆炸掉落的升级零件以及僚机等等。
enemy 从ship继承,是多有敌人飞机的基类,加入了敌人的行为控制。
player 从ship继承,是玩家控制飞机的类,也可以作为玩家其他机体的基类,主要是玩家控制飞机的行为。
其他小的物体的类,比如一些障碍物,装饰品等都是通过上面的类简单继承出来的,还有各种各样的敌人飞机,是通过继承敌机类,通过控制飞机类的基本属性,修改行为方式,继承一些特殊的子弹来实现的。
游戏类的形成思路
往往新手对于类没什么概念,也不太会用继承关系得到比较丰富的对象。而是针对某一个物体,写出单独的类。这样,实际上是可以的,只是每一次拓展,都需要复制粘贴很多的东西,修改多,而且代码比较混乱,某一种方法如果错误了,需要调整所有的代码。
对于新人,完全可以分为两步走,第一步,按照就事论事的原则来做,比如一上来我们可以做一个玩家控制的飞机对象。实际上,我们基础教程的前几课就足够你用了。只不过,之前是组件式的编程,行为写在对象的外面。而这一次,行为写在对象的里面。
我们很容易就可以做出比如玩家、子弹、敌人这些对象。(基础课程我们制作的那种)
然后,我们可以看出,这些对象有很多共通的地方,比如加入循环,比如移动方式,绘制方式等。我们按其各自功能集合的包含性,就可以提炼出一些基类出来。然后我们将代码重写一次,就可以得到树状的类层级了。
熟练之后,我们需要从简单到复杂的进行思考,不同的对象的共性,方法等,然后正向的铺设层级树。
重点代码
基类更新部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function Base:update(dt) if self.destroyed then return end self:translate(dt) if self.anim then self.anim:update(dt) end if self.tweens then for i,tween in pairs(self.tweens) do tween:update(dt) end end end function Base:draw() love.graphics.setColor(self.color) if self.anim then self.anim:draw(self.x,self.y,self.rot,self.scaleX,self.scaleY,self.anchorX,self.anchorY) end if self.image then if self.quad then love.graphics.draw(self.image,self.quad,self.x,self.y,self.rot - math.pi/2, self.scaleX,self.scaleY,self.anchorX,self.anchorY) else love.graphics.draw(self.image,self.x,self.y,self.rot - math.pi/2, self.scaleX,self.scaleY,self.anchorX,self.anchorY) end end if DEBUG then love.graphics.setColor(255, 0, 128) love.graphics.circle(self.x,self.y,5) end end
|
collidable对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| function collidable:getAABB() local w = self.scaleX*self.w/1.5 local h = self.scaleY*self.h/1.5 local x = self.x - w/2 local y = self.y - h/2 return x,y,w,h end function collidable:setPosition(l,t) self.x = l + self.scaleX*self.w/3 self.y = t + self.scaleX*self.h/3 end function collidable:initBump() game.world:add(self,self:getAABB()) end function collidable:destroy() if not self.destroyed then game.world:remove(self) end base.destroy(self) end function collidable.collidefilter(me,other) return bump.Response_Cross end function collidable:collision(cols) end function collidable:updateBump() local ox,oy = self:getAABB() local tx,ty ,cols = game.world:move(self,ox,oy,self.collidefilter) self:setPosition(tx,ty) self:collision(cols) end
|
ship类的武器系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| function ship:weaponUpdate(dt) for _,weapon in ipairs(self.weapons) do weapon.timer = weapon.timer - dt end end function ship:fire(gun,offx,offy,rot) if gun then gun(self,offx,offy,rot) return end for _,weapon in ipairs(self.weapons) do if weapon.timer < 0 then weapon.bullet(self,weapon.offx,weapon.offy,weapon.rot) weapon.timer = weapon.cd end end end function ship:damage(p) if self.overwhelming then return end self.hp = self.hp - p if self.hp<0 then self:destroy() end end function ship:destroy() collidable.destroy(self) Boom(self,self.scaleX*2) Frag(self) end function ship:update(dt) collidable.update(self,dt) for _,fire in ipairs(self.fireAnims) do fire:update(dt) end self:weaponUpdate(dt) if self.behavior then self:behavior() end end
|
玩家player类中的一些方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| function player:onDestroy() delay:new(2,function() game:gameover() end) end function player:keyControl() local down = love.keyboard.isDown local dt = love.timer.getDelta() if down("w") then self.y = self.y - self.speed * dt elseif down("s") then self.y = self.y + self.speed * dt end if down("a") then self.x = self.x - self.speed * dt elseif down("d") then self.x = self.x + self.speed * dt end if down("space") then self:fire() end end function player:limit() if self.x<16 then self.x = 16 elseif self.x>484 then self.x = 484 end if self.y<16 then self.y = 16 elseif self.y>784 then self.y = 784 end end function player:collision(cols) for i,col in ipairs(cols) do local other = col.other if other.tag == "enemy" then self:damage(other.hp) other:damage(self.hp) elseif other.tag == "item" then self:getItem(other) other:destroy() end end end
|
敌人enmey类
1 2 3 4 5 6 7 8 9 10 11 12 13
| function enemy:behavior() if self.state ~= "ok" then self.vx = self.speed * math.sin(self.rot) self.vy = -self.speed * math.cos(self.rot) self.state = "ok" end self:fire() end function enemy:outTest() if self.y>1000 or self.y<-1000 then self:destroy() end if self.x<-500 or self.x>1000 then self:destroy() end end
|
子弹类bullet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Bullet.canbeTarget = { bullet = false, player = true, enemy = true, item = false } function Bullet:collision(cols) for _,col in ipairs(cols) do local other = col.other if self.canbeTarget[other.tag] and self.parent.tag~= other.tag then self:destroy() other:damage(self.damage) end end end
|
好啦,基本类都差不多了,实际上,我们每建立一个类,我们就可以把他们扔到场景上测试一下。下一章我们将介绍如何通过摆放和生成这些类的实例来组成游戏。